using System;
using System.Collections.Generic;
using System.Text;

namespace FontKonverter {
    /// <summary>
    /// Reorder unicode string accordding unicode order
    /// </summary>
    public class LegacyReorder : IReOrder {
        #region Variables
        //# important character to test in order to form a cluster
        public const char SRAAA = '\u17B6';
        public const char SRAE = '\u17C1';
        public const char SRAOE = '\u17BE';
        public const char SRAOO = '\u17C4';
        public const char SRAYA = '\u17BF';
        public const char SRAIE = '\u17C0';
        public const char SRAAU = '\u17C5';
        public const char SRAII = '\u17B8';
        public const char SRAU = '\u17BB';
        public const char TRIISAP = '\u17CA';
        public const char MUUSIKATOAN = '\u17C9';
        public const char SAMYOKSANNYA = '\u17D0';

        public const char LA = '\u17A1';
        public const char NYO = '\u1789';
        public const char BA = '\u1794';
        public const char YO = '\u1799';
        public const char SA = '\u179F';
        public const char COENG = '\u17D2';
        public const string CORO = "\u17D2\u179A";
        public const string CONYO = "\u17D2\u1789";
        public const char SRAOM = '\u17C6';
        public const char MARK = '\u17EA';

        public const int CC_RESERVED = 0;
        public const int CC_CONSONANT = 1;    //# Consonant of type 1 or independent vowel
        public const int CC_CONSONANT2 = 2;    //# Consonant of type 2
        public const int CC_CONSONANT3 = 3;    //# Consonant of type 3
        public const int CC_ZERO_WIDTH_NJ_MARK = 4;    //# Zero Width non joiner character (0x200C)
        public const int CC_CONSONANT_SHIFTER = 5;
        public const int CC_ROBAT = 6;    //# Khmer special diacritic accent -treated differently in state table
        public const int CC_COENG = 7;    //# Subscript consonant combining character
        public const int CC_DEPENDENT_VOWEL = 8;
        public const int CC_SIGN_ABOVE = 9;
        public const int CC_SIGN_AFTER = 10;
        public const int CC_ZERO_WIDTH_J_MARK = 11;   //# Zero width joiner character
        public const int CC_COUNT = 12;   //# This is the number of character classes

        public const int CF_CLASS_MASK = 0x0000FFFF;

        public const int CF_CONSONANT = 0x01000000;   //# flag to speed up comparing
        public const int CF_SPLIT_VOWEL = 0x02000000;   //# flag for a split vowel -> the first part is added in front of the syllable
        public const int CF_DOTTED_CIRCLE = 0x04000000;   //# add a dotted circle if a character with this flag is the first in a
        //# syllable
        public const int CF_COENG = 0x08000000;   //# flag to speed up comparing
        public const int CF_SHIFTER = 0x10000000;   //# flag to speed up comparing
        public const int CF_ABOVE_VOWEL = 0x20000000;   //# flag to speed up comparing

        public const string DOTCIRCLE = "";
        #region position flags
        public const int CF_POS_BEFORE = 0x00080000;
        public const int CF_POS_BELOW = 0x00040000;
        public const int CF_POS_ABOVE = 0x00020000;
        public const int CF_POS_AFTER = 0x00010000;
        public const int CF_POS_MASK = 0x000f0000;
        #endregion

        //# simple classes, they are used in the state table (in this file) to control the length of a syllable
        //# they are also used to know where a character should be placed (location in reference to the base character)
        //# and also to know if a character, when independently displayed, should be displayed with a dotted-circle to
        //# indicate error in syllable construction
        #region ...
        public const int _xx = CC_RESERVED;
        public const int _sa = CC_SIGN_ABOVE | CF_DOTTED_CIRCLE | CF_POS_ABOVE;
        public const int _sp = CC_SIGN_AFTER | CF_DOTTED_CIRCLE | CF_POS_AFTER;
        public const int _c1 = CC_CONSONANT | CF_CONSONANT;
        public const int _c2 = CC_CONSONANT2 | CF_CONSONANT;
        public const int _c3 = CC_CONSONANT3 | CF_CONSONANT;
        public const int _rb = CC_ROBAT | CF_POS_ABOVE | CF_DOTTED_CIRCLE;
        public const int _cs = CC_CONSONANT_SHIFTER | CF_DOTTED_CIRCLE | CF_SHIFTER;
        public const int _dl = CC_DEPENDENT_VOWEL | CF_POS_BEFORE | CF_DOTTED_CIRCLE;
        public const int _db = CC_DEPENDENT_VOWEL | CF_POS_BELOW | CF_DOTTED_CIRCLE;
        public const int _da = CC_DEPENDENT_VOWEL | CF_POS_ABOVE | CF_DOTTED_CIRCLE | CF_ABOVE_VOWEL;
        public const int _dr = CC_DEPENDENT_VOWEL | CF_POS_AFTER | CF_DOTTED_CIRCLE;
        public const int _co = CC_COENG | CF_COENG | CF_DOTTED_CIRCLE;
        #endregion

        //# split vowel
        public const int _va = _da | CF_SPLIT_VOWEL;
        public const int _vr = _dr | CF_SPLIT_VOWEL;

        //# Character class tables
        //# _xx character does not combine into syllable, such as numbers, puntuation marks, non-Khmer signs...
        //# _sa Sign placed above the base
        //# _sp Sign placed after the base
        //# _c1 Consonant of type 1 or independent vowel (independent vowels behave as type 1 consonants)
        //# _c2 Consonant of type 2 (only RO)
        //# _c3 Consonant of type 3
        //# _rb Khmer sign robat u17CC. combining mark for subscript consonants
        //# _cd Consonant-shifter
        //# _dl Dependent vowel placed before the base (left of the base)
        //# _db Dependent vowel placed below the base
        //# _da Dependent vowel placed above the base
        //# _dr Dependent vowel placed behind the base (right of the base)
        //# _co Khmer combining mark COENG u17D2, combines with the consonant or independent vowel following
        //#     it to create a subscript consonant or independent vowel
        //# _va Khmer split vowel in wich the first part is before the base and the second one above the base
        //# _vr Khmer split vowel in wich the first part is before the base and the second one behind (right of) the base
        #region KhmerCharClasses
        private int[] khmerCharClasses = new int[]{
            _c1, _c1, _c1, _c3, _c1, _c1, _c1, _c1, _c3, _c1, _c1, _c1, _c1, _c3, _c1, _c1, //# 1780 - 178F
            _c1, _c1, _c1, _c1, _c3, _c1, _c1, _c1, _c1, _c3, _c2, _c1, _c1, _c1, _c3, _c3, //# 1790 - 179F
            _c1, _c3, _c1, _c1, _c1, _c1, _c1, _c1, _c1, _c1, _c1, _c1, _c1, _c1, _c1, _c1, //# 17A0 - 17AF
            _c1, _c1, _c1, _c1, _dr, _dr, _dr, _da, _da, _da, _da, _db, _db, _db, _va, _vr, //# 17B0 - 17BF
            _vr, _dl, _dl, _dl, _vr, _vr, _sa, _sp, _sp, _cs, _cs, _sa, _rb, _sa, _sa, _sa, //# 17C0 - 17CF
            _sa, _sa, _co, _sa, _xx, _xx, _xx, _xx, _xx, _xx, _xx, _xx, _xx, _sa, _xx, _xx, //# 17D0 - 17DF
        };
        #endregion

        #region khmerStateTable
        private int[][] khmerStateTable = {
                //# xx  c1  c2  c3 zwnj cs  rb  co  dv  sa  sp zwj
                new int[]{ 1,  2,  2,  2,  1,  1,  1,  6,  1,  1,  1,  2}, //#  0 - ground state
                new int[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, //#  1 - exit state (or sign to the right of the syllable)
                new int[]{-1, -1, -1, -1,  3,  4,  5,  6, 16, 17,  1, -1}, //#  2 - Base consonant
                new int[]{-1, -1, -1, -1, -1,  4, -1, -1, 16, -1, -1, -1}, //#  3 - First ZWNJ before a register shifter It can only be followed by a shifter or a vowel
                new int[]{-1, -1, -1, -1, 15, -1, -1,  6, 16, 17,  1, 14}, //#  4 - First register shifter
                new int[]{-1, -1, -1, -1, -1, -1, -1, -1, 20, -1,  1, -1}, //#  5 - Robat
                new int[]{-1,  7,  8,  9, -1, -1, -1, -1, -1, -1, -1, -1}, //#  6 - First Coeng
                new int[]{-1, -1, -1, -1, 12, 13, -1, 10, 16, 17,  1, 14}, //#  7 - First consonant of type 1 after coeng
                new int[]{-1, -1, -1, -1, 12, 13, -1, -1, 16, 17,  1, 14}, //#  8 - First consonant of type 2 after coeng
                new int[]{-1, -1, -1, -1, 12, 13, -1, 10, 16, 17,  1, 14}, //#  9 - First consonant or type 3 after ceong
                new int[]{-1, 11, 11, 11, -1, -1, -1, -1, -1, -1, -1, -1}, //# 10 - Second Coeng (no register shifter before)
                new int[]{-1, -1, -1, -1, 15, -1, -1, -1, 16, 17,  1, 14}, //# 11 - Second coeng consonant (or ind. vowel) no register shifter before  
                new int[]{-1, -1, -1, -1, -1, 13, -1, -1, 16, -1, -1, -1}, //# 12 - Second ZWNJ before a register shifter
                new int[]{-1, -1, -1, -1, 15, -1, -1, -1, 16, 17,  1, 14}, //# 13 - Second register shifter
                new int[]{-1, -1, -1, -1, -1, -1, -1, -1, 16, -1, -1, -1}, //# 14 - ZWJ before vowel
                new int[]{-1, -1, -1, -1, -1, -1, -1, -1, 16, -1, -1, -1}, //# 15 - ZWNJ before vowel
                new int[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, 17,  1, 18}, //# 16 - dependent vowel
                new int[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  1, 18}, //# 17 - sign above
                new int[]{-1, -1, -1, -1, -1, -1, -1, 19, -1, -1, -1, -1}, //# 18 - ZWJ after vowel
                new int[]{-1,  1, -1,  1, -1, -1, -1, -1, -1, -1, -1, -1}, //# 19 - Third coeng
                new int[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  1, -1}  //# 20 - dependent vowel after a Robat
            };
        #endregion

        #endregion
        
        // Possible combination for sra E
        private static Dictionary<char, char> m_SraECombining = null;
        public LegacyReorder() {
            if (m_SraECombining == null) {
                #region ...
                m_SraECombining = new Dictionary<char, char>();
                m_SraECombining.Add(SRAOE, SRAII);
                m_SraECombining.Add(SRAYA, SRAYA);
                m_SraECombining.Add(SRAIE, SRAIE);
                m_SraECombining.Add(SRAOO, SRAAA);
                m_SraECombining.Add(SRAAU, SRAAU);
                #endregion
            }
        }
        #region IReOrder Members
        /// <summary>
        /// 
        /// </summary>
        /// <param name="uniChar">one unicode character</param>
        /// <returns>integer which is the Khmer type of the character or 0</returns>
        public int GetCharValue(char? uniChar) {
            if (!uniChar.HasValue)
                return 0;

            //if (type(uniChar) != unicode)
            //    raise TypeError('only accept unicode character')
            //if (len(uniChar) != 1)
            //    raise TypeError('only accept one character, but ' + str(len(uniChar)) + ' chars found.')

            //ch = ord(uniChar[0])
            int ch = (char)uniChar;
            if (ch >= 0x1780) {
                ch -= 0x1780;
                if (ch < khmerCharClasses.Length)
                    return khmerCharClasses[ch];
            }
            return 0;
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="input">input string of unicode cluster to reorder</param>
        /// <returns>the visual based cluster (legacy style) string</returns>
        public string ReOrder(string sin) {
            //HACK: not sure how to port?
            //if (type(sin) != unicode)
            //    raise TypeError('only accept unicode string')
            StringBuilder result = new StringBuilder();
            char? reserved = null;
            string signAbove = "";
            char? signAfter = null;
            string baseChar = "";
            char? robat = null;
            string shifter = "";
            string vowelBefore = "";
            string vowelBelow = "";
            string vowelAbove = "";
            string vowelAfter = "";
            string coeng1 = "";
            string coeng2 = "";
            bool coeng = false;
            bool shifterAfterCoeng = false;

            int cursor = 0;
            int state = 0;
            int charCount = sin.Length;

            while (cursor < charCount) {
                #region flus cluster
                reserved = null;
                signAbove = "";
                signAfter = null;
                baseChar = "";
                robat = null;
                shifter = "";
                vowelBefore = "";
                vowelBelow = "";
                vowelAbove = "";
                vowelAfter = "";
                coeng = false;
                coeng1 = "";
                coeng2 = ""; 
                #endregion

                shifterAfterCoeng = false;

                while (cursor < charCount) {
                    char curChar = sin[cursor];
                    int kChar = GetCharValue(curChar);
                    int charClass = kChar & CF_CLASS_MASK;
                    state = khmerStateTable[state][charClass];

                    if (state < 0)
                        break;

                    #region Collecting cluster
                    if (kChar == _xx) {
                        reserved = curChar;
                    } else if (kChar == _sa) {          //# Sign placed above the base
                        signAbove = curChar.ToString();
                    } else if (kChar == _sp) {          //# Sign placed after the base
                        signAfter = curChar;
                    } else if ((kChar == _c1) || (kChar == _c2) || (kChar == _c3)) {    //# Consonant
                        if (coeng) {
                            if (string.IsNullOrEmpty(coeng1))
                                coeng1 = COENG.ToString() + curChar.ToString();
                            else
                                coeng2 = COENG.ToString() + curChar.ToString();

                            coeng = false;
                        } else
                            baseChar = curChar.ToString();
                    } else if (kChar == _rb) {            //# Khmer sign robat u17CC
                        robat = curChar;
                    } else if (kChar == _cs) {           //# Consonant-shifter
                        if (!string.IsNullOrEmpty(coeng1))
                            shifterAfterCoeng = true;
                        shifter = curChar.ToString();
                    } else if (kChar == _dl) {            //# Dependent vowel placed before the base
                        vowelBefore = curChar.ToString();
                    } else if (kChar == _db) {            //# Dependent vowel placed below the base
                        vowelBelow = curChar.ToString();
                    } else if (kChar == _da) {            //# Dependent vowel placed above the base
                        vowelAbove = curChar.ToString();
                    } else if (kChar == _dr) {            //# Dependent vowel placed behind the base
                        vowelAfter = curChar.ToString();
                    } else if (kChar == _co) {          //# Khmer combining mark COENG
                        coeng = true;
                    } else if (kChar == _va) {            //# Khmer split vowel, see _da
                        vowelBefore = SRAE.ToString();
                        vowelAbove = m_SraECombining[curChar].ToString();
                    } else if (kChar == _vr) {          //# Khmer split vowel, see _dr
                        vowelBefore = SRAE.ToString();
                        vowelAfter = m_SraECombining[curChar].ToString();
                    }
                    #endregion
                    
                    cursor++;
                }    //# end of while (a cluster has found)

                //# logic of vowel
                //# determine if right side vowel should be marked
                if (!string.IsNullOrEmpty(coeng1) && !string.IsNullOrEmpty(vowelBelow)) {
                    vowelBelow = MARK.ToString() + vowelBelow;
                } else if ((baseChar.Equals(LA.ToString()) || baseChar.Equals(NYO.ToString())) 
                    && !string.IsNullOrEmpty(vowelBelow)) {
                    vowelBelow = MARK.ToString() + vowelBelow;
                } else if (!string.IsNullOrEmpty(coeng1) 
                    && !string.IsNullOrEmpty(vowelBefore) 
                    && !string.IsNullOrEmpty(vowelAfter)) {
                    vowelAfter = MARK.ToString() + vowelAfter;
                }

                //# logic when cluster has coeng
                //# should coeng be located on left side
                string coengBefore = "";
                if (coeng1.Equals(CORO)) {
                    coengBefore = coeng1;
                    coeng1 = "";
                } else if (coeng2.Equals(CORO)) {
                    coengBefore = MARK.ToString() + coeng2;
                    coeng2 = "";
                }

                if (!string.IsNullOrEmpty(coeng1) || !string.IsNullOrEmpty(coeng2)) {
                    //# NYO must change to other form when there is coeng
                    if (baseChar.Equals(NYO.ToString())) {
                        baseChar = MARK.ToString() + baseChar;

                        //# coeng NYO must be marked
                        if (coeng1.Equals(CONYO))
                            coeng1 = MARK.ToString() + coeng1;
                    }

                    if (!string.IsNullOrEmpty(coeng1) 
                        && !string.IsNullOrEmpty(coeng2))
                        coeng2 = MARK.ToString() + coeng2;
                }

                //# logic of shifter with base character
                if (!string.IsNullOrEmpty(baseChar) 
                    && !string.IsNullOrEmpty(shifter)) {
                    //# special case apply to BA only
                    if (!string.IsNullOrEmpty(vowelAbove) 
                        && (baseChar.Equals(BA.ToString())) 
                        && (shifter.Equals(TRIISAP.ToString()))) {
                        vowelAbove = MARK.ToString() + vowelAbove;

                    } else if (!string.IsNullOrEmpty(vowelAbove)) {
                        shifter = MARK.ToString() + shifter;

                    } else if ((signAbove.Equals(SAMYOKSANNYA.ToString())) 
                        && (shifter.Equals(MUUSIKATOAN.ToString()))) {
                        shifter = MARK.ToString() + shifter;

                    } else if (!string.IsNullOrEmpty(signAbove) 
                        && !string.IsNullOrEmpty(vowelAfter)) {
                        shifter = MARK.ToString() + shifter;

                    } else if (!string.IsNullOrEmpty(signAbove)) {
                        signAbove = MARK.ToString() + signAbove;

                    }

                    //# add another mark to shifter
                    if (!string.IsNullOrEmpty(coeng1) 
                        && (!string.IsNullOrEmpty(vowelAbove) 
                            || !string.IsNullOrEmpty(signAbove)))
                        shifter = MARK.ToString() + shifter;

                    if (baseChar.Equals(LA.ToString()) 
                        || baseChar.Equals(NYO.ToString()))
                        shifter = MARK.ToString() + shifter;
                }

                //# uncomplete coeng
                if (coeng && string.IsNullOrEmpty(coeng1)) {
                    coeng1 = COENG.ToString();

                } else if (coeng && string.IsNullOrEmpty(coeng2)) {
                    coeng2 = MARK.ToString() + COENG;

                }

                //# render DOTCIRCLE for standalone sign or vowel
                if ((string.IsNullOrEmpty(baseChar)) && 
                    (!string.IsNullOrEmpty(vowelBefore) 
                    || !string.IsNullOrEmpty(coengBefore) 
                    || robat.HasValue || !string.IsNullOrEmpty(shifter) 
                    || !string.IsNullOrEmpty(coeng1) || !string.IsNullOrEmpty(coeng2) 
                    || !string.IsNullOrEmpty(vowelAfter) || !string.IsNullOrEmpty(vowelBelow) 
                    || !string.IsNullOrEmpty(vowelAbove) || !string.IsNullOrEmpty(signAbove) 
                    || signAfter.HasValue))
                    baseChar = DOTCIRCLE;

                //# place of shifter
                string shifter1 = "";
                string shifter2 = "";
                if (shifterAfterCoeng)
                    shifter2 = shifter;
                else
                    shifter1 = shifter;

                bool specialCaseBA = false;
                if (baseChar.Equals(BA.ToString()) 
                    && (vowelAfter.Equals(SRAAA.ToString()) 
                        || vowelAfter.Equals(SRAAU.ToString()) 
                        || vowelAfter.Equals(MARK.ToString() + SRAAA) 
                        || vowelAfter.Equals(MARK.ToString() + SRAAU)
                    )){
                    //# SRAAA or SRAAU will get a MARK if there is coeng, redefine to last char
                    vowelAfter = vowelAfter[vowelAfter.Length-1].ToString();
                    specialCaseBA = true;
                    if(!string.IsNullOrEmpty(coeng1)){
                        char lastCoeng = coeng1[coeng1.Length-1];
                        if (lastCoeng == BA || lastCoeng == YO || lastCoeng == SA)
                            specialCaseBA = false;
                    }
                }

                //# cluster formation
                if (specialCaseBA)
                    result.Append(CombineBA(signAbove, signAfter, baseChar, robat, vowelBefore, vowelBelow, vowelAbove, vowelAfter, coeng1, coeng2, coengBefore, shifter1, shifter2));
                else
                    result.Append(Combine(signAbove, signAfter, baseChar, robat, vowelBefore, vowelBelow, vowelAbove, vowelAfter, coeng1, coeng2, coengBefore, shifter1, shifter2));
                
                //    result += cluster + reserved
                if(reserved.HasValue)
                    result.Append(reserved);

                state = 0;
            }//# end of while

            return result.ToString();
        }

        private string Combine(string signAbove, char? signAfter, string baseChar, char? robat, string vowelBefore, string vowelBelow, string vowelAbove, string vowelAfter, string coeng1, string coeng2, string coengBefore, string shifter1, string shifter2) {
            StringBuilder sb = new StringBuilder();
            //Console.WriteLine("!spcialCaseBA: signAbove={0}, signAfter={1}, baseChar={2}, robat={3}, vowelBefore={4}, vowelBelow={5}, vowelAbove={6}, vowelAfter={7}, coeng1={8}, coeng2={9}, coengBefore={10}, shifter1={11},shifter2={12}", 
            //                                signAbove, signAfter, baseChar, robat, vowelBefore, vowelBelow, vowelAbove, vowelAfter, coeng1, coeng2, coengBefore, shifter1, shifter2);

            if (!string.IsNullOrEmpty(vowelBefore)) sb.Append(vowelBefore);
            if (!string.IsNullOrEmpty(coengBefore)) sb.Append(coengBefore);
            if (!string.IsNullOrEmpty(baseChar)) sb.Append(baseChar);
            if (robat.HasValue) sb.Append(robat);
            if (!string.IsNullOrEmpty(shifter1)) sb.Append(shifter1);
            if (!string.IsNullOrEmpty(coeng1)) sb.Append(coeng1);
            if (!string.IsNullOrEmpty(coeng2)) sb.Append(coeng2);
            if (!string.IsNullOrEmpty(shifter2)) sb.Append(shifter2);
            if (!string.IsNullOrEmpty(vowelBelow)) sb.Append(vowelBelow);
            if (!string.IsNullOrEmpty(vowelAbove)) sb.Append(vowelAbove);
            if (!string.IsNullOrEmpty(vowelAfter)) sb.Append(vowelAfter);
            if (!string.IsNullOrEmpty(signAbove)) sb.Append(signAbove);
            if(signAfter.HasValue) sb.Append(signAfter);
            
            return sb.ToString();
        }

        private string CombineBA(string signAbove, char? signAfter, string baseChar, char? robat, string vowelBefore, string vowelBelow, string vowelAbove, string vowelAfter, string coeng1, string coeng2, string coengBefore, string shifter1, string shifter2) {
            StringBuilder sb = new StringBuilder();
            //Console.WriteLine("specialCaseBA: signAbove={0}, signAfter={1}, baseChar={2}, robat={3}, vowelBefore={4}, vowelBelow={5}, vowelAbove={6}, vowelAfter={7}, coeng1={8}, coeng2={9}, coengBefore={10}, shifter1={11}, shifter2={12}", 
            //                                signAbove, signAfter, baseChar, robat, vowelBefore, vowelBelow, vowelAbove, vowelAfter, coeng1, coeng2, coengBefore, shifter1, shifter2);

            if (!string.IsNullOrEmpty(vowelBefore)) sb.Append(vowelBefore);
            if (!string.IsNullOrEmpty(coengBefore)) sb.Append(coengBefore);
            if (!string.IsNullOrEmpty(baseChar)) sb.Append(baseChar);
            if (!string.IsNullOrEmpty(vowelAfter)) sb.Append(vowelAfter);
            if (robat.HasValue) sb.Append(robat);
            if (!string.IsNullOrEmpty(shifter1)) sb.Append(shifter1);
            if (!string.IsNullOrEmpty(coeng1)) sb.Append(coeng1);
            if (!string.IsNullOrEmpty(coeng2)) sb.Append(coeng2);
            if (!string.IsNullOrEmpty(shifter2)) sb.Append(shifter2);
            if (!string.IsNullOrEmpty(vowelBelow)) sb.Append(vowelBelow);
            if (!string.IsNullOrEmpty(vowelAbove)) sb.Append(vowelAbove);
            if (!string.IsNullOrEmpty(signAbove)) sb.Append(signAbove);
            if (signAfter.HasValue) sb.Append(signAfter);

            return sb.ToString();
        }

        #endregion
    }
}
